#include "GraphicsManager.h"

GraphicsManager *GraphicsManager::graphicsManager;

GraphicsManager::GraphicsManager()
{
	graphicsManager = this;
};

GraphicsManager::~GraphicsManager()
{
};

/**
 *@Content : Get GraphicsManager
 *@Input :	NULL
 *@Output : A GraphicsManager
**/
GraphicsManager *GraphicsManager::GetGraphicsManager()
{
	return graphicsManager;
};

/**
 *@Content : Init Graphics
**/
void GraphicsManager::InitGraphics()
{
	UseEffect(effect_id::SHADER3D);
	glEnable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
};

/**
 *@Content : Compile Vertex Shader and Fragment Shader
 *@Input :	- source : shader file
 *			- type : + GL_VERTEX_SHADER : Vertex shader
					 + GL_FRAGMENT_SHADER : Fragment shader
 *@Output : ID of shader after compile
**/
int GraphicsManager::CompileShader(const char *source, int type)
{
	GLint status = 0;
	GLuint shader = glCreateShader(type);
	glShaderSource(shader, 1, &source, NULL);
	glCompileShader(shader);
	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);

#if defined(DEBUG)
	GLint logLength;
	glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
	if (logLength > 0)
	{
		GLchar *log = new GLchar[logLength];
		glGetShaderInfoLog(shader, logLength, &logLength, log);
		LOGI("Shader compile log:\n%s", log);
		delete []log;
	}
#endif

	if (status == GL_FALSE)
	{
		LOGE("Error When Compiling Source\n");
		shader = 0; // clear the shader
	}

	return shader;
};

/**
 *@Content : Load effect of shader
 *@Input :	- effect : A ShaderEffect
 *@Output : ID of shadereffect after compile, a program
**/
int GraphicsManager::LoadEffect(ShaderEffect &effect)
{
	GLint status = 0;

	GLuint vertexShader = 0, fragmentShader = 0, program = 0;

	vertexShader = CompileShader(effect.vertexSource, GL_VERTEX_SHADER);
	int ik = strlen(effect.vertexSource);
	fragmentShader = CompileShader(effect.fragmentSource, GL_FRAGMENT_SHADER);
	int ih = strlen(effect.fragmentSource);
	if (!(vertexShader && fragmentShader))
	{
		return ERROR_COMPILE_SHADER;
	}

	program = glCreateProgram();

	glAttachShader(program, vertexShader);
	glAttachShader(program, fragmentShader);

	glBindAttribLocation(program, ATTRIB_POSITION, "aPosition");
	glBindAttribLocation(program, ATTRIB_COLOR, "aColor");
	glBindAttribLocation(program, ATTRIB_NORMAL, "aNormal");
	glBindAttribLocation(program, ATTRIB_TEXCOORD, "aTexcoord");

	glLinkProgram(program);
	glGetProgramiv(program, GL_LINK_STATUS, &status);    

	if (status == GL_FALSE)
	{
		LOGE("Couldn't Link Shaders: Error #%d\n'%s'\n-----\n%s", glGetError(), effect.vertexSource, effect.fragmentSource);
		return ERROR_LINK_SHADER;
	}

	effect.effectId = program;


	for (int i = 0; effect.uniformNames[i]; i++) 
	{
		effect.uniformsId[i] = glGetUniformLocation(program, effect.uniformNames[i]);
		LOGI("'%s' => %d\n", effect.uniformNames[i], effect.uniformsId[i]);
	}

	effect.FreeCacheData();

	return ERROR_NO;
};

/**
 *@Content : Use effect to draw
 *@Input :	- effectId : A effect ID
 *@Output : NULL
**/
void GraphicsManager::UseEffect(int effectId)
{ 
	currentEffect = ResourcesManager::GetResourcesManager()->GetShader(effectId);
	glUseProgram(currentEffect->effectId);
}

/**
 *@Content : Set Matrix (4 x 4) to uniform of shadereffect
 *@Input :	- uniformId : Id of uniform variable
			- matrix
 *@Output : NULL
**/
void GraphicsManager::SetMatrix(int uniformId, const Matrix &matrix)
{
	glUniformMatrix4fv((*currentEffect)[uniformId], 1, GL_FALSE, matrix.value);
};

/**
 *@Content : Set vector position to uniform of shadereffect
 *@Input :	- uniformId : Id of uniform variable
			- vector
 *@Output : NULL
**/
void GraphicsManager::SetPosition(int uniformId, const Vector3 &vector)
{
	glUniform3f((*currentEffect)[uniformId], vector.x, vector.y, vector.z);
};

/**
 *@Content : Set color(r, g, b, a) to uniform of shadereffect
 *@Input :	- uniformId : Id of uniform variable
			- color
 *@Output : NULL
**/
void GraphicsManager::SetColor(int uniformId, const Color &color)
{
	glUniform4f((*currentEffect)[uniformId], color.r, color.g, color.b, color.a);
};

/**
 *@Content : Set int to uniform of shadereffect
 *@Input :	- uniformId : Id of uniform variable
			- int
 *@Output : NULL
**/
void GraphicsManager::SetInt(int uniformId, int value)
{
	glUniform1i((*currentEffect)[uniformId], value);
};

/**
 *@Content : Set float to uniform of shadereffect
 *@Input :	- uniformId : Id of uniform variable
			- float
 *@Output : NULL
**/
void GraphicsManager::SetFloat(int uniformId, float value)
{
	glUniform1f((*currentEffect)[uniformId], value);
};

/**
 *@Content : Set texture
 *@Input :	- uniformId : Id of uniform variable
			- textureId : Id of texture
			- pos : in MyConsts.h
			- type : GL_TEXTURE_2D in default
 *@Output : NULL
**/
void GraphicsManager::SetTexture(int uniformId, unsigned int textureId, int pos, int type)
{
	glActiveTexture(GL_TEXTURE0 + pos);
	glBindTexture(type, textureId);
	glUniform1i((*currentEffect)[uniformId], pos);
};

/**
 *@Content : Set texture
 *@Input :	- uniformId : Id of uniform variable
			- textureId : data of image file
			- pos : in MyConsts.h
			- isRGBA : is RGBA or RGB
				+ 1 : RGBA
				+ 0 : RGB
 *@Output : NULL
**/
void GraphicsManager::SetTexture(int uniformId, Texture2D &texture2D, int pos, bool isRGBA)
{
	if (!texture2D.textureName)
	{
		glGenTextures(1, &texture2D.textureName);
		glBindTexture(GL_TEXTURE_2D, texture2D.textureName);
		if(texture2D.channels == 3)
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture2D.width, texture2D.height, 0, GL_RGB, GL_UNSIGNED_BYTE, texture2D.data);
		else if(texture2D.channels == 4)
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture2D.width, texture2D.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture2D.data);

		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

		glGenerateMipmap(GL_TEXTURE_2D);
		glBindTexture(GL_TEXTURE_2D, 0);
		// SAFE_DEL_ARRAY(texture2D.data);
	}

	SetTexture(uniformId, texture2D.textureName, pos);
};

/**
 *@Content : Create buffer, set float array to graphic card; this is vertex vector, nomal vector and texCoord vector
 *@Input :	- values : float array
			- size : array measure
 *@Output : ID of array in memory
**/
unsigned int GraphicsManager::CreateBuffer(float *values, int size)
{
	unsigned int buffer;
	glGenBuffers(1, &buffer);
	glBindBuffer(GL_ARRAY_BUFFER, buffer);
	glBufferData(GL_ARRAY_BUFFER, size, values, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	return buffer;
};

/**
 *@Content : Delete float array in card Graphic
 *@Input :	- buffer : ID of array in memory
 *@Output : NULL
**/
void GraphicsManager::DeleteBuffer(unsigned int buffer)
{
	glDeleteBuffers(1, &buffer);
};

/**
 *@Content : Set attribute of variable for shaders that is used
 *@Input :	- position :
			- bufferId :
			- size :
 *@Output : NULL
**/
void GraphicsManager::SetAttribute(int position, unsigned int bufferId, int size)
{
	glEnableVertexAttribArray(position);
	glBindBuffer(GL_ARRAY_BUFFER, bufferId);
	glVertexAttribPointer(position, size, GL_FLOAT, GL_FALSE, 0, 0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
};

/**
 *@Content : Generate frame buffer (mipmap), Gen a picture to smaller pictures
 *@Input :	- framebuffer :
			- width :
			- height :
 *@Output : NULL
**/
void GraphicsManager::GenFrameBuffer(FrameBuffer &framebuffer, int width, int height)
{
	glGenFramebuffers(1, &framebuffer.buffer);
	glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.buffer);
	framebuffer.width = width;
	framebuffer.height = height;

	if (framebuffer.depthAsTexture)
	{
		glGenTextures(1, &framebuffer.depthBuffer);
		glBindTexture(GL_TEXTURE_2D, framebuffer.depthBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, NULL);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, framebuffer.depthBuffer, 0);
	}
	else
	{
		glGenRenderbuffers(1, &framebuffer.depthBuffer);
		glBindRenderbuffer(GL_RENDERBUFFER, framebuffer.depthBuffer);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, framebuffer.depthBuffer);
	}

	if (framebuffer.colorAsTexture)
	{
		glGenTextures(1, &framebuffer.colorBuffer);
		glBindTexture(GL_TEXTURE_2D, framebuffer.colorBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, NULL);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, framebuffer.colorBuffer, 0);
	}
	else
	{
		glGenRenderbuffers(1, &framebuffer.colorBuffer);
		glBindRenderbuffer(GL_RENDERBUFFER, framebuffer.colorBuffer);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB565, width, height);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, framebuffer.colorBuffer);
	}

	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	switch(status)
	{
	case GL_FRAMEBUFFER_COMPLETE:
		LOGE("Framebuffer complete.\n");
		break;
	case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
		LOGE("[ERROR] Framebuffer incomplete: Attachment is NOT complete.\n");
		break;
	case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
		LOGE("[ERROR] Framebuffer incomplete: No image is attached to FBO.\n");
		break;
	case GL_FRAMEBUFFER_UNSUPPORTED:
		LOGE("[ERROR] Unsupported by FBO implementation.\n");
		break;
	default:
		LOGE("[ERROR] Unknown error:%d.\n", status);
		break;
	};
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
};

/**
 *@Content : Use frame buffer
 *@Input :	- framebuffer :
 *@Output : NULL
**/
void GraphicsManager::UseFrameBuffer(FrameBuffer *frameBuffer)
{
	if (frameBuffer)
	{
		glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer->buffer);
		//glViewport(0, 0, frameBuffer->width, frameBuffer->height);
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
	}
	else
	{
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		//glViewport(0, 0, SCREEN_W, SCREEN_H);
	}
};

/**
 *@Content : Use frame buffer, free display to draw
 *@Input :	- framebuffer :
 *@Output : NULL
**/
void GraphicsManager::DeleteFrameBuffer(FrameBuffer &frameBuffer)
{
	if (frameBuffer.depthAsTexture)
	{
		glDeleteTextures(1, &frameBuffer.depthBuffer);
	}
	else
	{
		glDeleteRenderbuffers(1, &frameBuffer.depthBuffer);
	}


	if (frameBuffer.colorAsTexture)
	{
		glDeleteTextures(1, &frameBuffer.colorBuffer);
	}
	else
	{
		glDeleteRenderbuffers(1, &frameBuffer.colorBuffer);
	}
};

/**
 *@Content : input a picture of Cube to memory
 *@Input :	- framebuffer :
 *@Output : NULL
**/
unsigned int GraphicsManager::CreateTextureCube(int textureIndex)
{
	ResourcesManager *resourcesManager = ResourcesManager::GetResourcesManager();
	unsigned int textureId;
	glGenTextures(1, &textureId);

	glBindTexture(GL_TEXTURE_CUBE_MAP, textureId);
	for (int i = 0; i < 6; i++)
	{
		Texture2D *texture2D = resourcesManager->GetTexture(textureIndex + i);
		unsigned int format = texture2D->channels == 3 ? GL_RGB : GL_RGBA;
		glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, texture2D->width, texture2D->height, 0, format, GL_UNSIGNED_BYTE, texture2D->data);
		SAFE_DEL_ARRAY(texture2D->data);
	}   
	glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
	return textureId;
};
